C#で学んだこと
step7



デリゲート(delegate:代行者)

デリゲートとは、要するに都合に合わせたメソッドを格納(カプセル化)するもの。 デリゲートは、継承やインターフェイスや多相性と同じく、 どのメソッドを実装するかを、実行まで遅らせるのに役立つ。 デリゲートが便利なのは、ソースコードを書いている時に、ある特定の所で、 ある動作を行わなければならないことが(そのreturn型と仮パラメータも)分かっているけど、 具体的な実装が事前に分からない場合のとき。 その一例がGUI(イベント駆動のグラフィカルユーザーインターフェイス)
  • デリゲートと同じreturn型、同じ仮パラメータの複数実装されたメソッドを、 実行時に同じデリゲートで表現できる。これも抽象メソッドと似ている。
  • デリゲートには、実行時に適切なメソッドが代入されカプセル化される。 そしてデリゲートが呼び出されると、デリゲートは自分がカプセル化しているメソッドに、 実際の処理を「委譲」(delegate)する。
イベントとデリゲートには密接な関係がある。 あるボタンがクリックされたら、デリゲートが呼び出されるようにしておけば、 そのボタンクリックによって、どのメソッドを呼び出すべきかの判断を、実行時まで遅らせることができる。 そして、実行時に特定のメソッドを、そのデリゲートに代入しておけば、 そのボタンが押される度に、指定したメソッドがデリゲートから呼び出される。 C#でイベント駆動プログラム実装には、極めて重要な概念
p.302

デリゲートの概要

  • デリゲートはクラスの一種であり、参照型。
  • System.Delegateクラスを基底クラスとして派生。
  • ちなみに、オブジェクトの実体化したものはインスタンスだが、デリゲートにその様な別名はない。 ⇒クラスにあたるものもデリゲートだし、実体化したものもデリゲート。
p.303

デリゲートの書き方

  • デリゲートはSystem.Delegateから派生するが、クラスの派生のようにコロン(:)を使わずdelegateキーワードを使う。 ★ public delegate double 計算(int x , int y); //デリゲート定義 この行は、return型がdoubleでint型の2つのパラメータを持つメソッドなら、何でもカプセル化できるという デリゲートを定義している。
  • デリゲートはクラスの一種なので、定義は通常のクラス定義を書く場所と同じ
  • クラスに対して使うのと同じアクセス修飾子を指定することができ、意味も同じ
  • 先ほどの★行は、1つのユーザー定義型を表している。 この型を使って、計算型の変数を新しく宣言できる。 計算 my計算;
  • my計算は、計算型のどんなインスタンスも参照できる。
  • デリゲートのインスタンスを新しく作成するには、通常通りnewキーワードを使う。 my計算 = new 計算(メソッド名); //メソッドを格納(カプセル化) ⇒こういう風にも書ける。計算 my計算 = new 計算(メソッド名); ここでメソッド名とは、デリゲートインスタンスがカプセル化するメソッドのこと。 そのメソッドは、デリゲート定義と同じreturn型と仮パラメータを持たなければならない名前は自由
  • 呼び出し方は、通常のメソッド呼び出しと同じ方法。 結果 = my計算(15,20) //カプセル化したメソッドに引数を渡す感じ
////////////////////////////////////////////////////////////////////// using System; class Calculator { public delegate double 計算(int x , int y); //デリゲート定義 ここにも書けるのか public static double Sum(int 数1 , int 数2) //同じreturn型にすべく、double型を採用 { return 数1 + 数2; } public static void Main() { double 結果; Math myMath = new Math(); 計算 my計算 = new 計算(myMath.Average); //メソッドを格納 結果 = my計算(10,20); //デリゲートに格納したメソッドに引数を渡す感じ Console.WriteLine("my計算に10,20を渡した結果: {0}",結果); my計算 = new 計算(Sum); //新たに別のメソッドを格納 結果 = my計算(10 , 20); Console.WriteLine("my計算に10,20を渡した結果: {0}",結果); } } class Math { public double Average(int 数値1, int 数値2) { return (数値1 + 数値2) / 2; } } 結果::: my計算に10,20を渡した結果: 15 my計算に10,20を渡した結果: 30 ////////////////////////////////////////////////////////////////////// 上コードでは、まだデリゲートのメリットを活かしきれていない。 〜その他の書き方決まり〜
  • デリゲートのコンストラクタには、常に引数を1個だけ渡さなければならない
  • 引数として渡すメソッドが、他クラスのメソッドならば、クラス名も添えて、引数として渡す
p.303

デリゲートの有効な使い方

デリゲートは他にも特徴として、
  • デリゲートの配列を作成できる
  • デリゲートを引数として受け取るメソッドを作成できる
////////////////////////////////////////////////////////////////////// using System; delegate double 計算実行(double num); //デリゲート定義 class RocketCalculator { private 計算実行[ ] 計算列; //デリゲート型配列 要素にカプセル化されたメソッドが入る private int 計算カウンタ; public RocketCalculator() { 計算カウンタ = 0; 計算列 = new 計算実行[10]; } public void 計算を追加する(計算実行 次の計算) { 計算列[計算カウンタ] = 次の計算; //カプセル化したメソッドを渡し、 //別デリゲートにカプセル化 計算カウンタ++; } public double 計算を開始する(double 値) { Console.WriteLine("最初の数値: {0}", 値); for (int i = 0 ; i < 計算カウンタ; i++) { 値 = 計算列[i](値) //引数のdouble型値を、デリゲートのメソッドに渡す //デリゲートにカプセル化したメソッドの発火(実行) } return 値; } } class Math { public static 計算実行 MinusTwenty実行 = new 計算実行(MinusTwenty); //カプセル化 public static 計算実行 TimesTwo実行 = new 計算実行(TimesTwo); pubcli static 計算実行 PlusTen実行 = new 計算実行(PlusTen); public static double MinusTwenty(double number) { Console.WriteLine("20を引く"); return number -20; } public static double TimesTwo(double number) { Console.WriteLine("2倍する"); return number * 2; } public static double PlusTen(double number) { Console.WriteLine("10を足す"); return number + 10; } } public class Tester { public static void Main() { double 最初の数値; double 最終結果; string 応答; RocketCalculator calculator = new RocketCalculator(); Console.Write("最初の数値を指定してください"); 最初の数値 = Convert.ToDouble(Console.ReadLine()); Console.WriteLine("以下のオプションを繰り返し選択して"); Console.WriteLine("演算処理のシーケンスを作ってください"); Console.WriteLine("M)inus Twenty ... 20を引く"); Console.WriteLine("T)imes Two ... 2倍する"); Console.WriteLine("P)lus Ten ... 10を足す"); Console.WriteLine("計算を開始するにはCを入力してください\n"); do { 応答 = Console.ReadLine().ToUpper(); switch(応答) { case: "M" calculator.計算を追加する(Math.MinusTwenty実行); //メソッドのカプセルを引数として渡す Console.WriteLine("20を引く演算を追加しました"); break; case : "T" calculator.計算を追加する(Math.TimesTwo実行); Console.WriteLine("2倍する演算を追加しました"); break; case : "P" calculator.計算を追加する(Math.PlusTen実行); Console.WriteLine("10を足す演算を追加しました"); break; case : "C" calculator.計算を開始する(最初の数値); Console.WriteLine("最終結果: {0}", 最終結果); break; default: Console.WriteLine("選択が無効です。もう一度どうぞ。"); break; } }while(応答 != "C"); } } 結果::: 最初の数値を指定してください 以下のオプションを繰り返し選択して 演算処理のシーケンスを作ってください M)inus Twenty ... 20を引く T)imes Two ... 2倍する P)lus Ten ... 10を足す 計算を開始するにはCを入力してください t<enter> 2倍する演算を追加しました t<enter> 2倍する演算を追加しました m<enter> 20を引く演算を追加しました p<enter> 10を足す演算を追加しました t<enter> 2倍する演算を追加しました c<enter> 最初の数値: 10 2倍する 2倍する 20を引く 10を足す 2倍する 最終結果: 60  ////////////////////////////////////////////////////////////////////// Mathクラスではデリゲートのどれかにアクセスするたびに、いちいちインスタンスを作成するのが面倒なので、 static宣言してある。
p.305

マルチキャストデリゲート

複数のメソッドをカプセル化することができる、マルチキャストデリゲート(かけもち代行者)がある。 ⇒特に便利なのが、イベント駆動のGUIプログラムでは、1回のボタンクリックイベントで  2つ以上のメソッドを呼び出したい場合 ⇒前コードのように、デリゲートのコレクション(配列)の代わりになる
  • マルチキャストデリゲートが呼び出されると、カプセル化しているメソッドを1つずつ次々に呼び出す
  • voidをreturn型とするデリゲートは、自動的にマルチキャストデリゲートになる。 ⇒何故なら複数の戻り値を返すことができないから
 
p.309

マルチキャストデリゲートの書き方

  • delegate void あいさつ(); //デリゲート宣言 あいさつ 僕のあいさつ = new あいさつ(SayThankYou); //普通のメソッドのカプセル化 以上は、ここまで学んだシングルキャストデリゲートだが、 +演算子を使うと、2つのマルチキャストデリゲートを一体化でき、 その結果は同じ型のマルチキャストデリゲートに代入できる。↓↓ あいさつ 君のあいさつ = new あいさつ(SayGoodMorning); あいさつ 我らのあいさつ = 僕のあいさつ + 君のあいさつ; //マルチキャストデリゲートの一体化
  • また、+=演算子を使ってマルチキャストデリゲートに、他のマルチキャストゲートを加えることもできる。 我らのあいさつ += new あいさつ(SayGoodNight); (我らのあいさつ = 我らのあいさつ + new あいさつ(SayGoodNight);)同じ意味 こうすると、我らのあいさつは、今までカプセル化したメソッドに加え、 SayGoodNightメソッドも含むことができる。
  • 同じように、−演算子や、−=演算子を使って、マルチキャストデリゲートから、 1個のマルチキャストデリゲートを削除することができる。
////////////////////////////////////////////////////////////////////// using System; class MulticastTester { delegate void あいさつ(); public static void Main() { あいさつ 僕のあいさつ = new あいさつ(SayThankYou); Console.WriteLine("僕のあいさつ:"); 僕のあいさつ(); あいさつ 君のあいさつ = new あいさつ(SayGoodMorning); Console.WriteLine("\n君のあいさつ:"); 君のあいさつ(); あいさつ 我らのあいさつ = 僕のあいさつ + 君のあいさつ; Console.WriteLine("\n我らのマルチキャストあいさつ:"); 我らのあいさつ(); 我らのあいさつ += new あいさつ(SayGoodnight); Console.WriteLine("\nGoodnightを加えたマルチキャストあいさつ:"); 我らのあいさつ(); 我らのあいさつ = 我らのあいさつ - 君のあいさつ; Console.WriteLine("\n君のを差し引いたマルチキャストあいさつ:"); 我らのあいさつ(); 我らのあいさつ -= 僕のあいさつ; Console.WriteLine("\n僕のも差し引いたマルチキャストあいさつ:"); 我らのあいさつ(); } public static void SayThankYou() { Console.WriteLine("Thank you!"); } public static void SayGoodMorning() { Console.WriteLine("Good morning!"); } public static void SayGoodnight() { Console.WriteLine("Goodnight"); } } 結果::: 僕のあいさつ: Thank you! 君のあいさつ: Good morning! 我らのマルチキャストあいさつ: Thank you! Good morning! Goodnightを加えたマルチキャストあいさつ: Thank you! Good morning! Goodnight 君のを差し引いたマルチキャストあいさつ: Thank you! Goodnight 僕のも差し引いたマルチキャストあいさつ: Goodnight ////////////////////////////////////////////////////////////////////// 演算子の+、+=、−、−=を使えるのは、マルチキャストデリゲートのみに限る。
p.309

イベント

イベントの例としては、ボタンのクリック、タイマの1分経過、プリンタの印刷終了などがある。 イベントの生成は、イベントの「発火」とも呼ばれる。 イベントとは、要するにイベント処理のプロセスに合わせて、少しだけ特殊化されたマルチキャストデリゲート。 イベントが他のオブジェクトから発行されたとき、その通知を受けるように、 1つ以上のオブジェクトが、そのイベントに「参加(登録)」することができる。 その参加したオブジェクト(参加者)は、 自分が参加したイベントを処理するためのイベントハンドラと呼ばれるメソッドを 含んでいないといけない。 イベントが発火すると、参加しているオブジェクトのイベントハンドラが次々に呼び出される。 参加者は、何種類も異なった型のイベントに参加でき、複数のイベントハンドラを持てる。 マルチキャストデリゲートは、このイベント処理プロセスを実装して、 イベント発行者とイベント参加者の間にリンクを形成するのに最適。
p.312

イベント発行者と、参加者に分割する利点

  • 発行者を書くプログラマは、参加者について詳しく知る必要がない
  • 参加者を書くプログラマも、発行者について詳しく知る必要がない
  • 参加者型のオブジェクトが、実行時何かのイベントに参加したり、脱退することも、 コーディングによって関係が固定されていないため、簡単に行うことが出来る。
p.313

イベント駆動型のプログラムを書く

イベントは、マルチキャストデリゲートにeventキーワードを付けて宣言する。 マルチキャストデリゲートのevent版のようなイメージ。 もし次のように、MoveRequestというマルチキャストデリゲートが定義してあれば、 public delegate void MoveRequest(object sender , MoveRequestEventArgs e); //引数が気になるが・・ 次のように、MyMoveRequestという通常のマルチキャストデリゲートを宣言する代わりに、 public MoveRequest MyMoveRequest; //通常のマルチキャストデリゲートの宣言 eventキーワードを追加して、イベントをOnMoveRequestというイベントを宣言できる。 public event MoveRequest OnMoveRequest; check
  • イベント名は慣習により、どれもOnで始まる
  • デリゲート定義と、それに対応するイベント宣言は、イベント発行側のクラスに書く
  • デリゲートと、イベントハンドラは、イベント参加側のクラスに書く
p.313

イベントを書くには、 イベント宣言 イベントハンドラ宣言 デリゲート定義 を書かなければいけない。
p.

イベントの発火

イベントの実行(発火)は、デリゲート呼び出しと同様に、イベント呼び出しによって行われる。 発行側で次のコードを実行すると、イベントが実行(呼び出)される。 OnMoveRequest(senderObject , someEventArguments);
p.314

マルチキャストデリゲートと、イベントの主な違い

イベントでは、+=演算子と、−=演算子しか使えない。 覚えなくても良いが、、 コンパイラがeventキーワードを見つけると、そのイベントは暗黙のうちにprivate宣言され、 +=演算子と−=演算子でアクセスされる2つのpublicプロパティが作られる。
p.314

イベントを実装するデリゲートの決まり

イベントを実装するデリゲートは、規約により次の2つのパラメータを持たなければいけない
  • 1つ目のパラメータの型は、System.Objectobject型のパラメータは、発行側オブジェクトが参加側オブジェクトのイベントハンドラに、  どのオブジェクトの中で、イベントが発火されたかを通知するのに使われる。   (したがってsenderすなわち「送り主」が使われることが多い)      オブジェクト自身への参照はthisキーワードを使って、   発行側で行うイベントの発火は次のように書く。   OnMoveRequest(this , new MoveRequestEventArgs(MoveRequestType.FastForward));      
  • 2つ目のパラメータの型は、System.EventArgs型(またはその派生クラス) ⇒イベントに特有の情報を参加者に渡すことができる。
public delegate void MoveRequest(object sender , MoveRequestEventArgs e); ////////////////////////////////////////////////////////////////////// using System; enum MoveRequestType {FastForward, SlowForward, Reverse}; class MoveRequestEventArgs : EventArgs { private MoveRequestType request; //列挙型変数の宣言 public MoveRequestEventArgs(MoveRequestType initRequest) : base() { request = initRequest; } public MoveRequestType Request { get {return request; } } } class GameController //イベント発行側クラス { public delegate void MoveRequest(object sender , MoveRequestEventArgs e); //デリゲート定義 public event MoveRequest OnMoveRequest; //イベント宣言 Car [ ] gameCars = new Car [10]; string carName; int speedParam = 0; int carCounter = 0; int carNumber = 0; public void Run() { string answer; Console.WriteLine("以下のメニューから選択してください"); Console.WriteLine("A)dd new car ゲームカーを追加"); Console.WriteLine("C)ar subscribe ゲームカー,イベントに参加"); Console.WriteLine("U)nsubscribe ゲームカー,イベントからの脱退"); Console.WriteLine("L)ist cars... ゲームカーのリスト"); Console.WriteLine("F)ast forward 高速前進"); Console.WriteLine("S)low forward 緩速前進"); Console.WriteLine("R)everse ... 後退"); Console.WriteLine("T)erminate .. 終了"); do { Console.WriteLine("コマンドは:"); answer = Console.ReadLine().ToUpper(); swith(answer) { case "A": Console.Write("新しいゲームカーの名前は: "); carName = Console.ReadLine(); Console.Write("新しいゲームカーの速度パラメータは: "); speedParam = Convert.ToInt32(Console.ReadLine()); gameCars[carCounter] = new Car(speedParam , carName); carCounter++; break; case "C": Console.Write("イベントに参加させるゲームカーの配列 インデックスは: "); carNumber = Convert.ToInt32(Console.ReadLine()); gameCars[carCounter].Subscribe(this); //配列の要素が境界外になるとエラーを出す break; case "U": Console.Write("イベントから脱退させるゲームカーの配列 インデックスは: "); carNumber = Convert.ToInt32(Console.ReadLine()); gameCars[carCounter].Unsubscribe(this); break; case "L": for(int i=0 ; i< carCounter; i++) { Console.WriteLine(gameCars[i]); } break; case "F": if (OnMoveRequest != null) //イベントにイベントハンドラが //カプセル化されているか確認 { OnMoveRequest(this , new MoveRequestEventArgs( MoveRequestType.FastForward)); //イベントハンドラ発火 } break; case "S": if (OnMoveRequest != null) { OnMoveRequest(this , new MoveRequestEventArgs( MoveRequestType.FastForward)); } break; case "R": if (OnMoveRequest != null) { OnMoveRequest(this , new MoveRequestEventArgs( MoveRequestType.Reverse)); } break; case "T": break; default: //ユーザーの入力に対してチェックできる Console.WriteLine("選択が無効です。もう一度どうぞ"); break; } }while(answer != "T"); } } class Car { private int 距離; private int 速度パラメータ; private string 車名; public Car(int 初期速度パラメータ , string 初期車名) { 速度パラメータ = 初期速度パラメータ; 距離 = 0; 車名 = 初期車名; } public void Subsribe(GameController controller) { controller.OnMoveRequest += new GameController.MoveRequest( MoveRequestHandler); //イベントハンドラのカプセル化(追加) //クラスの中にデリゲートがあるので、こういう書き方になる。 } public void Unsubscribe(GameController controller) { controller.OnMoveRequest -= new GameController.MoveRequest( MoveRequestHandler) //イベント型デリゲートから削除 } public void MoveRequestHandler(object sender , MoveRequestEventArgs e) //object型変数senderは今回使わないが、記述するのが決まり { swicth (e.Request) // { case MoveRequestType.SlowForward : 距離 += 速度パラメータ; Console.WriteLine("車名: " + 車名 + "緩速前進。 距離: " + 距離); break; case MoveRequestType.FastForward : 距離 += 速度パラメータ * 2; Console.WriteLine("車名: " + 車名 + "高速前進。 距離: " + 距離); break; case MoveRequest.Reverse : 距離 -= 5; Console.WriteLine("車名: " + 車名 + "後退。 距離: " + 距離); break; } } public override string ToString() { return 車名; } } class Tester { public static void Main() { GameController controller = new GameController(); controller.Run(); } } 結果::: 以下のメニューから選択してください A)dd new car ゲームカーを追加" C)ar subscribe ゲームカー,イベントに参加 U)nsubscribe ゲームカー,イベントからの脱退 L)ist cars... ゲームカーのリスト F)ast forward 高速前進 S)low forward 緩速前進 R)everse ... 後退 T)erminate .. 終了 コマンドは: A<enter> 新しいゲームカーの名前は: Volvo<enter> 新しいゲームカーの速度パラメータは: 20<enter> コマンドは: F<enter> コマンドは: C<enter> イベントに参加させるゲームカーの配列インデックスは: 1<enter> コマンドは: S<enter> 車名: Volvo 緩速前進。 距離:60 車名: Lotus 緩速前進。 距離:40 コマンドは: U<enter> イベントから脱退させるゲームカーの配列インデックスは: 0<enter> コマンドは: R<enter> 車名: Lotus 後退。 距離:35 コマンドは: L<enter> Volvo Lotus コマンドは: C<enter> イベントに参加させるゲームカーの配列インデックスは: 0<enter> コマンドは: R<enter> 車名: Volvo 後退。 距離:60 車名: Lotus 後退。 距離:40 コマンドは: T<enter> //////////////////////////////////////////////////////////////////////
p.315

p.

p.

p.

p.

p.

p.

p.

p.